<?php

declare(strict_types=1);

namespace Dreamcat\ApiDoc\Swagger;

use Dreamcat\ApiDoc\Enums\DataType;
use Dreamcat\ApiDoc\Enums\SwaggerJsonMode;
use Dreamcat\ApiDoc\Enums\SwaggerParamType;
use Dreamcat\ApiDoc\Enums\SwaggerV2Type;
use Dreamcat\ApiDoc\Popo\ApiDefinition;
use Dreamcat\ApiDoc\Popo\BodyDefinition;
use JetBrains\PhpStorm\ExpectedValues;
use JetBrains\PhpStorm\Pure;
use RuntimeException;
use stdClass;

/**
 * api定义转swagger json的工具
 * @author vijay
 */
class Define2Swagger
{
    /** @var array 生成的swagger json */
    private array $json;
    /** @var array 生成的tag下标 */
    private array $tagsMap = [];
    /** @var SwaggerJsonMode swagger json 的模式 */
    private SwaggerJsonMode $swaggerJsonMode;

    /**
     * @param string $title 文档标题
     * @param string $version 版本号
     */
    public function __construct(string $title, string $version = "last")
    {
        $this->json = [
            "swagger" => "2.0",
            "info" => [
                "title" => $title,
                "version" => $version,
            ],
            "basePath" => "/",
            "tags" => [],
            "schemes" => ["http"],
            "paths" => [],
        ];
        $this->swaggerJsonMode = SwaggerJsonMode::SWAGGER();
    }

    /**
     * 设置swagger 版本号
     * @param string $version 版本号
     * @return static 当前对象
     */
    public function setSwaggerVersion(string $version): Define2Swagger
    {
        $this->json["swagger"] = $version;
        return $this;
    }

    /**
     * 设置文档标题
     * @param string $title 文档标题
     * @return static 当前对象
     */
    public function setTitle(string $title): Define2Swagger
    {
        $this->json["info"]["title"] = $title;
        return $this;
    }

    /**
     * 设置版本号
     * @param string $version 版本号
     * @return static 当前对象
     */
    public function setVersion(string $version): Define2Swagger
    {
        $this->json["info"]["version"] = $version;
        return $this;
    }

    /**
     * 设置基准地址
     * @param string $basePath 基准地址
     * @return static 当前对象
     */
    public function setBasePath(string $basePath): Define2Swagger
    {
        $this->json["basePath"] = "/" . ltrim(trim($basePath), "/");
        return $this;
    }

    /**
     * @return array swagger json
     */
    #[Pure]
    public function getJson(): array
    {
        return $this->json;
    }

    /**
     * 添加一个接口文档定义
     * @param ApiDefinition $apiDefinition 一个接口文档定义
     * @return static 当前对象
     */
    public function addApiDefine(ApiDefinition $apiDefinition): Define2Swagger
    {
        $tag = $apiDefinition->getApiTag();
        if (!$tag) {
            $tag = "公共分类";
        }
        if (!isset($this->tagsMap[$tag])) {
            $this->tagsMap[$tag] = count($this->json["tags"]);
            $this->json["tags"][] = [
                "name" => $tag,
                "description" => "",
            ];
        }
        $apiInfo = [
            "tags" => [$tag],
            "summary" => $apiDefinition->getName(),
            "description" => $apiDefinition->getDesc(),
            "parameters" => [],
            "responses" => [],
        ];
        foreach ($apiDefinition->getHeaders() as $key => $definition) {
            $apiInfo = $this->addSimpleParams($apiInfo, $definition, $key, SwaggerParamType::HEADER());
        }
        foreach ($apiDefinition->getCookies() as $key => $definition) {
            # swagger 2.0 不支持cookie
            $desc = $definition->getDesc();
            if (!strlen($desc)) {
                $desc = "-";
            }
            $apiInfo["description"] .= "; cookie:{$key}, {$desc}";
        }
        # 路径变量类型只能是 "string", "number", "boolean", "integer", "array"
        foreach ($apiDefinition->getPathVariableDefinitionList() as $key => $definition) {
            $apiInfo = $this->addSimpleParams($apiInfo, $definition, $key, SwaggerParamType::PATH());
        }
        # get参数的类型只能是 "string", "number", "boolean", "integer", "array"
        foreach ($apiDefinition->getGetDefinitionList() as $key => $definition) {
            $apiInfo = $this->addSimpleParams($apiInfo, $definition, $key, SwaggerParamType::QUERY());
        }
        if ($apiDefinition->getBodyDefinition()) {
            $body = $this->formatBodyDefinition($apiDefinition->getBodyDefinition());
            $jsonBody = [
                "name" => "root",
                "in" => "body",
                "schema" => $body,
            ];
            if ($apiDefinition->getBodyDefinition()->isRequired()) {
                $jsonBody["required"] = true;
            }
            $apiInfo["parameters"][] = $jsonBody;
        }
        $ret = $apiDefinition->getResponseDefinition();
        if ($ret) {
            $apiInfo["responses"]["200"] = [
                "description" => "successful operation",
                "schema" => $this->formatBodyDefinition($ret),
            ];
            if (!$apiInfo["responses"]["200"]["schema"] || $ret->getDataType()->getValue() == DataType::UNKNOWN) {
                unset($apiInfo["responses"]["200"]["schema"]);
                $apiInfo["responses"]["200"]["description"] = "mixed response";
            }
        } else {
            $apiInfo["responses"]["200"] = ["description" => "void response"];
        }
        $this->json["paths"][$apiDefinition->getUri()][strtolower($apiDefinition->getMethod())] = $apiInfo;
        return $this;
    }

    /**
     * 添加简易参数
     * @param array $apiInfo 已定义的api信息
     * @param BodyDefinition $definition 参数定义
     * @param string $key 参数所属名称
     * @param SwaggerParamType $swaggerParamType swagger参数类型
     * @return array 完善的api信息
     */
    private function addSimpleParams(
        array $apiInfo,
        BodyDefinition $definition,
        string $key,
        SwaggerParamType $swaggerParamType
    ): array {
        $body = $this->formatBodyDefinition($definition);
        $bodys = $this->checkSimpleParam($body, $key, $definition->isRequired());
        foreach ($bodys as $body) {
            $body["in"] = $swaggerParamType->getValue();
            if (isset($body["example"]) && strlen($body["example"])) {
                if (strlen($body["description"])) {
                    $body["description"] .= "; ";
                }
                $body["description"] .= "示例：{$body["example"]}";
            }
            unset($body["example"], $body["mock"]);
            $apiInfo["parameters"][] = $body;
        }
        return $apiInfo;
    }

    /**
     * 将参数定义转为swagger json
     * @param BodyDefinition $bodyDefinition 参数定义
     * @return array swagger json
     */
    #[Pure]
    private function formatBodyDefinition(BodyDefinition $bodyDefinition): array
    {
        $info = match ($bodyDefinition->getDataType()->getValue()) {
            DataType::ENUM => $this->formatEnum($bodyDefinition),
            DataType::OBJECT => $this->formatObject($bodyDefinition),
            DataType::MAP => $this->formatMap($bodyDefinition),
            DataType::INTEGER => $this->formatInt($bodyDefinition),
            DataType::STRING => $this->formatString($bodyDefinition),
            DataType::FLOAT => $this->formatFloat($bodyDefinition),
            DataType::BOOL => $this->formatBool($bodyDefinition),
            DataType::ARRAY => $this->formatArray($bodyDefinition),
            default => [
                "type" => DataType::STRING,
                "description" => "不限类型; " . $bodyDefinition->getDesc(),
            ]
        };
        if ($bodyDefinition->getMock() && $this->getSwaggerJsonMode()->getValue() == SwaggerJsonMode::YAPI) {
            $info["mock"]["mock"] = $bodyDefinition->getMock();
        }
        if ($bodyDefinition->getExample()) {
            $info["example"] = $bodyDefinition->getExample();
        }
        if (!is_null($bodyDefinition->getDefault())) {
            $info["default"] = $bodyDefinition->getDefault();
        }
        return $info;
    }

    /**
     * 格式化枚举转为swagger json
     * @param BodyDefinition $bodyDefinition 参数定义
     * @return array swagger json
     * @noinspection PhpPureFunctionMayProduceSideEffectsInspection
     */
    #[Pure]
    private function formatEnum(BodyDefinition $bodyDefinition): array
    {
        # 先把枚举定义成string，之后考虑根据枚举值判断
        $info = [
            "type" => SwaggerV2Type::STRING,
            "description" => $bodyDefinition->getDesc(),
            "enum" => [],
            "enumDesc" => "",
        ];
        foreach ($bodyDefinition->getEnums() as $val => $desc) {
            $info["enum"][] = strval($val);
            $info["enumDesc"] .= "{$val} : {$desc};\n";
        }
        if ($this->getSwaggerJsonMode()->getValue() == SwaggerJsonMode::SWAGGER) {
            $enumDesc = $info["enumDesc"];
            if (strlen($info["description"])) {
                $enumDesc = "\n{$enumDesc}";
            }
            $info["description"] .= $enumDesc;
            unset($info["enumDesc"]);
        }
        return $info;
    }

    /**
     * @return SwaggerJsonMode swagger json 的模式
     */
    public function getSwaggerJsonMode(): SwaggerJsonMode
    {
        return $this->swaggerJsonMode;
    }

    /**
     * @param SwaggerJsonMode $swaggerJsonMode swagger json 的模式
     * @return static 当前对象
     */
    public function setSwaggerJsonMode(SwaggerJsonMode $swaggerJsonMode): Define2Swagger
    {
        $this->swaggerJsonMode = $swaggerJsonMode;
        return $this;
    }

    /**
     * 格式化对象转为swagger json
     * @param BodyDefinition $bodyDefinition 参数定义
     * @return array swagger json
     * @noinspection PhpPureFunctionMayProduceSideEffectsInspection
     */
    #[Pure]
    private function formatObject(BodyDefinition $bodyDefinition): array
    {
        $info = [
            "type" => SwaggerV2Type::OBJECT,
            "description" => $bodyDefinition->getDesc(),
            "properties" => [],
            "required" => [],
        ];
        foreach ($bodyDefinition->getObjectProperties() as $key => $objectProperty) {
            $info["properties"][$key] = $this->formatBodyDefinition($objectProperty);
            if ($objectProperty->isRequired()) {
                $info["required"][] = $key;
            }
        }
        if (!$info["required"]) {
            unset($info["required"]);
        }
        if (!$info["properties"]) {
            unset($info["properties"]);
        }
        return $info;
    }

    /**
     * 格式化Map转为swagger json
     * @param BodyDefinition $bodyDefinition 参数定义
     * @return array swagger json
     */
    #[Pure]
    private function formatMap(BodyDefinition $bodyDefinition): array
    {
        $info = [
            "description" => [$bodyDefinition->getDesc()],
        ];
        if ($bodyDefinition->getMapKeyDesc()) {
            $info["description"][] = "key的定义：{$bodyDefinition->getMapKeyDesc()}";
        }
        if ($bodyDefinition->getMapValueDesc()) {
            $info["description"][] = "值的定义：{$bodyDefinition->getMapValueDesc()}";
        }
        $info["description"] = implode("; ", $info["description"]);

        if ($bodyDefinition->getChildDefinition()) {
            $info["type"] = SwaggerV2Type::ARRAY;
            $info["items"] = $this->formatBodyDefinition($bodyDefinition->getChildDefinition());
            $size = $bodyDefinition->getSize();
            if ($size) {
                if ($size->getSizeMax() !== null) {
                    $info["maxItems"] = $size->getSizeMax();
                }
                if ($size->getSizeMin() !== null) {
                    $info["minItems"] = $size->getSizeMin();
                }
            }
        } else {
            $info["type"] = SwaggerV2Type::OBJECT;
        }
        return $info;
    }

    /**
     * 格式化整数转为swagger json
     * @param BodyDefinition $bodyDefinition 参数定义
     * @return array swagger json
     */
    #[Pure]
    private function formatInt(BodyDefinition $bodyDefinition): array
    {
        return $this->formatNumber($bodyDefinition, SwaggerV2Type::INTEGER);
    }

    /**
     * 格式化数字转为swagger json
     * @param BodyDefinition $bodyDefinition 参数定义
     * @param string $type 类型
     * @return array swagger json
     */
    #[Pure]
    private function formatNumber(
        BodyDefinition $bodyDefinition,
        #[ExpectedValues(valuesFromClass: SwaggerV2Type::class)]
        string $type
    ): array {
        $info = [
            "type" => $type,
            "description" => $bodyDefinition->getDesc(),
        ];
        $scope = $bodyDefinition->getNumberScope();
        if ($scope) {
            if ($scope->getMax() !== null) {
                $info["maximum"] = $scope->getMax();
            }
            if ($scope->getMin() !== null) {
                $info["minimum"] = $scope->getMin();
            }
        }
        return $info;
    }

    /**
     * 格式化字符串转为swagger json
     * @param BodyDefinition $bodyDefinition 参数定义
     * @return array swagger json
     */
    #[Pure]
    private function formatString(BodyDefinition $bodyDefinition): array
    {
        $info = [
            "type" => SwaggerV2Type::STRING,
            "description" => $bodyDefinition->getDesc(),
        ];
        if ($bodyDefinition->getPattern()) {
            $info["pattern"] = $this->getPatternFromPhp($bodyDefinition->getPattern()->getPattern());
        }
        $size = $bodyDefinition->getSize();
        if ($size) {
            if ($size->getSizeMax() !== null) {
                $info["maxLength"] = $size->getSizeMax();
            }
            if ($size->getSizeMin() !== null) {
                $info["minLength"] = $size->getSizeMin();
            }
        }
        return $info;
    }

    /**
     * 从php正则转为常规正则
     * @param string $pattern
     * @return string
     * @noinspection PhpPureFunctionMayProduceSideEffectsInspection
     */
    #[Pure]
    private function getPatternFromPhp(string $pattern): string
    {
        if (strlen($pattern) < 2) {
            // @codeCoverageIgnoreStart
            throw new RuntimeException("约束的 PHP 正则表达式 {$pattern} 长度不足2");
            // @codeCoverageIgnoreEnd
        }
        $first = $pattern[0];
        $last = strrpos($pattern, $first, 1);
        if ($last === false) {
            // @codeCoverageIgnoreStart
            throw new RuntimeException("约束的 PHP 正则表达式格式非法 {$pattern}");
            // @codeCoverageIgnoreEnd
        }
        return substr($pattern, 1, $last - 1);
    }

    /**
     * 格式化浮点数转为swagger json
     * @param BodyDefinition $bodyDefinition 参数定义
     * @return array swagger json
     */
    #[Pure]
    private function formatFloat(BodyDefinition $bodyDefinition): array
    {
        return $this->formatNumber($bodyDefinition, SwaggerV2Type::NUMBER);
    }

    /**
     * 格式化布尔转为swagger json
     * @param BodyDefinition $bodyDefinition 参数定义
     * @return array swagger json
     */
    #[Pure]
    private function formatBool(BodyDefinition $bodyDefinition): array
    {
        return [
            "type" => SwaggerV2Type::BOOLEAN,
            "description" => $bodyDefinition->getDesc(),
        ];
    }

    /**
     * 格式化数组转为swagger json
     * @param BodyDefinition $bodyDefinition 参数定义
     * @return array swagger json
     */
    #[Pure]
    private function formatArray(BodyDefinition $bodyDefinition): array
    {
        $info = [
            "type" => SwaggerV2Type::ARRAY,
            "description" => $bodyDefinition->getDesc(),
        ];
        $child = $bodyDefinition->getChildDefinition();
        if ($child) {
            $info["items"] = $this->formatBodyDefinition($child);
        } else {
            $info["items"] = new stdClass();
        }
        $size = $bodyDefinition->getSize();
        if ($size) {
            if ($size->getSizeMax() !== null) {
                $info["maxItems"] = $size->getSizeMax();
            }
            if ($size->getSizeMin() !== null) {
                $info["minItems"] = $size->getSizeMin();
            }
        }
        return $info;
    }

    /**
     * 处理简易参数类型问题
     * @param array $body 已生成的定义
     * @param string $prefix 前缀路径
     * @param bool $require 是否必填
     * @return array 修改完善的定义
     * @note 由于get参数和路径变量的类型只能是"string", "number", "boolean", "integer", "array"，所以需要处理
     */
    private function checkSimpleParam(array $body, string $prefix, bool $require): array
    {
        switch ($body["type"]) {
            case SwaggerV2Type::STRING:
            case SwaggerV2Type::NUMBER:
            case SwaggerV2Type::BOOLEAN:
            case SwaggerV2Type::INTEGER:
                $body["required"] = $require;
                $body["name"] = $prefix;
                return [$body];
            case SwaggerV2Type::ARRAY:
                # todo 需要考虑怎么弄
                // @codeCoverageIgnoreStart
                return [$body];
            // @codeCoverageIgnoreEnd
            case SwaggerV2Type::OBJECT:
                $list = [];
                $req = array_flip($body["required"] ?? []);
                foreach ($body["properties"] as $name => $property) {
                    $list = array_merge(
                        $list,
                        $this->checkSimpleParam($property, "{$prefix}[{$name}]", isset($req[$name]))
                    );
                }
                return $list;
            default:
                // @codeCoverageIgnoreStart
                throw new RuntimeException("未知的类型 {$body["type"]}");
            // @codeCoverageIgnoreEnd
        }
    }
}

# end of file
